/*
 *    Copyright 2008-2009 The PoCo Team
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
package com.asemantics.poco.serializer.json;

import java.io.IOException;
import java.io.Writer;
import java.util.Stack;

/**
 * 
 * @author Simone Tripodi (simone.tripodi@gmail.com@gmail.com)
 * @version $Id: JsonWriter.java 259 2009-12-20 01:20:11Z simone.tripodi@gmail.com $
 */
final class JsonWriter {

    private final static char BEGIN_OBJ = '{';

    private final static char END_OBJ = '}';

    private final static char BEGIN_ARRAY = '[';

    private final static char END_ARRAY = ']';

    private final static char QUOTE = '\"';

    private final static char NAME_SEPARATOR = ':';

    private final static char VALUE_SEPARATOR = ',';

    private final static char SPACE = ' ';

    private final static char NEW_LINE = '\n';

    private final static char[] QUOTATION_MARK = "\\\"".toCharArray();

    private final static char[] REVERSE_SOLIDUS = "\\\\".toCharArray();

    private final static char[] SOLIDUS = "\\/".toCharArray();

    private final static char[] BACKSPACE = "\\b".toCharArray();

    private final static char[] FROMFEED = "\\f".toCharArray();

    private final static char[] NEWLINE = "\\n".toCharArray();

    private final static char[] CARRIAGE_RETURN = "\\r".toCharArray();

    private final static char[] HORIZONTAL_TAB = "\\t".toCharArray();

    private final static char[] ISO_HEADER = "\\u".toCharArray();

    private final static char[] HEX = "0123456789ABCDEF".toCharArray();

    private final Writer writer;

    private final boolean prettyPrint;

    private boolean keyStatus = false;

    private final Stack<StackElement> deepStack = new Stack<StackElement>();

    public JsonWriter(Writer writer) {
        this(writer, false);
    }

    public JsonWriter(Writer writer, boolean prettyPrint) {
        this.writer = writer;
        this.prettyPrint = prettyPrint;
    }

    public void startDocument() throws IOException {
    }

    public void beginObject() throws IOException {
        if (this.prettyPrint) {
            this.doPrettyPrint();
        }

        if (!this.deepStack.isEmpty()) {
            if (!this.deepStack.peek().isFirst() && !this.keyStatus) {
                this.printValueSeparator();
            } else {
                this.deepStack.peek().setFirst(false);
            }
        }

        this.deepStack.push(new StackElement());
        this.keyStatus = false;

        this.writer.write(BEGIN_OBJ);
    }

    public void beginArray() throws IOException {
        if (!this.deepStack.isEmpty()) {
            if (!this.deepStack.peek().isFirst() && !this.keyStatus) {
                this.printValueSeparator();
            } else {
                this.deepStack.peek().setFirst(false);
            }
        }
        this.deepStack.push(new StackElement());
        this.keyStatus = false;

        this.writer.write(BEGIN_ARRAY);
        if (this.prettyPrint) {
            this.writer.write(NEW_LINE);
        }
    }

    public void endArray() throws IOException {
        this.deepStack.pop();

        if (this.prettyPrint) {
            this.doPrettyPrint();
        }
        this.writer.write(END_ARRAY);
    }

    public void endObject() throws IOException {
        this.deepStack.pop();

        if (this.prettyPrint) {
            this.writer.write(NEW_LINE);
            this.doPrettyPrint();
        }
        this.writer.write(END_OBJ);
        if (this.prettyPrint) {
            this.writer.write(NEW_LINE);
        }
    }

    public void endDocument() throws IOException {
        if (this.prettyPrint) {
            this.writer.write(NEW_LINE);
        }
        this.writer.flush();
    }

    public void printKey(String key) throws IOException {
        if (this.deepStack.peek().isFirst()) {
            this.deepStack.peek().setFirst(false);
        } else {
            this.printValueSeparator();
        }
        this.keyStatus = true;

        if (this.prettyPrint) {
            this.writer.write(NEW_LINE);
            this.doPrettyPrint();
        }

        this.writer.write(QUOTE);
        escapeChars(key);
        this.writer.write(QUOTE);
        if (this.prettyPrint) {
            this.writer.write(SPACE);
            this.writer.write(NAME_SEPARATOR);
            this.writer.write(SPACE);
        } else {
            this.writer.write(NAME_SEPARATOR);
        }
    }

    public void printString(String value, boolean literal) throws IOException {
        this.keyStatus = false;

        if (!literal) {
            this.writer.write(QUOTE);
        }
        escapeChars(value);
        if (!literal) {
            this.writer.write(QUOTE);
        }
    }

    private void printValueSeparator() throws IOException {
        this.writer.write(VALUE_SEPARATOR);
    }

    /**
     * Write a string according to <a href="http://www.json.org/">grammar</a> rules
     * 
     * @param value
     * @return
     */
    private void escapeChars(String value) throws IOException {
        for (int i = 0; i < value.length(); ++i) {
            char current = value.charAt(i);

            switch (current) {
            case '"': // " quotation mark
                this.writer.write(QUOTATION_MARK); 
                break;

            case '\\': // \ reverse solidus
                this.writer.write(REVERSE_SOLIDUS);
                break;

            case '/': // / solidus
                this.writer.write(SOLIDUS);
                break;

            case '\b': // \b backspace
                this.writer.write(BACKSPACE);
                break;

            case '\f': // \f fromfeed
                this.writer.write(FROMFEED);
                break;

            case '\n': // \n newline
                this.writer.write(NEWLINE);
                break;

            case '\r': // \r carriage return
                this.writer.write(CARRIAGE_RETURN);
                break;

            case '\t': // \t horizontal tab
                this.writer.write(HORIZONTAL_TAB);
                break;

            default:
                if (Character.isISOControl(current)) {
                    this.writer.write(ISO_HEADER);
                    int currentInt = (int) current;
                    for (int j = 0; j < 4; ++j) {
                        int digit = (currentInt & 0xf000) >> 12;
                        this.writer.write(HEX[digit]);
                        currentInt <<= 4;
                    }
                } else {
                    this.writer.write(current);
                }
                break;
            }

        }
    }

    private void doPrettyPrint() throws IOException {
        for (int i = 0; i < ((this.deepStack.size() - 1) * 4); i++) {
            this.writer.write(SPACE);
        }
    }

}
